package com.hqd.jjvm.instructions.ref;

import com.hqd.jjvm.classformat.InstructionCode;
import com.hqd.jjvm.instructions.AbstractInstruction;
import com.hqd.jjvm.instructions.ArrayType;
import com.hqd.jjvm.instructions.InstructionType;
import com.hqd.jjvm.rtda.JThread;
import com.hqd.jjvm.rtda.heap.JClass;
import com.hqd.jjvm.rtda.heap.JField;
import com.hqd.jjvm.rtda.heap.JMethod;
import com.hqd.jjvm.rtda.heap.RuntimeConstantPool;
import com.hqd.jjvm.rtda.heap.ref.JClassRef;
import com.hqd.jjvm.rtda.heap.ref.JFieldRef;
import com.hqd.jjvm.rtda.heap.ref.JInterfaceMethodRef;
import com.hqd.jjvm.rtda.heap.ref.JMethodRef;
import com.hqd.jjvm.rtda.heap.state.JClassState;
import com.hqd.jjvm.rtda.obj.JObject;
import com.hqd.jjvm.util.ClassUtil;
import org.apache.commons.lang.ArrayUtils;

import java.io.IOException;
import java.util.Deque;

public class RefInstruction extends AbstractInstruction {


    public RefInstruction(JThread jThread) {
        super(jThread);
    }

    @Override
    public void execute(InstructionType instructionType) {
        JThread jThread = getJThread();
        JClass currentClass = jThread.getJvmStack().getTop().getJClass();
        RuntimeConstantPool runtimeConstantPool = currentClass.getConstantPool();
        switch (instructionType) {
            case GETSTATIC: {
                JField jField = getField(runtimeConstantPool, false);
                /**
                 * 不是static，抛出异常
                 */
                if (!jField.isStatic()) {
                    throw new IncompatibleClassChangeError("the field:" + jField.getName() + " is not static");
                }
                JClass jc = jField.getJClass();
                try {
                    if (jc.getState().equals(JClassState.FULLY_INITIALIZED) || jc.getState().equals(JClassState.BEING_INITIALIZED)) {
                        offerIncrement(3);
                        pushOperandStackObjVal(jc.getStaticVars()[jField.getSlotId()].getVal());
                    } else {
                        initClass(jThread, jc);
                    }

                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                break;
            }
            case PUTSTATIC: {
                JField jField = getField(runtimeConstantPool);
                /**
                 * 不是static，抛出异常
                 */
                JClass jc = jField.getJClass();
                if (!jField.isStatic()) {
                    throw new IncompatibleClassChangeError("the field:" + jField.getName() + " is not static");
                }
                if (jField.isFinal()) {
                    if (!currentClass.equals(jc) || !getJMethod().getName().equals("<clinit>")) {
                        throw new IllegalAccessError("the field:" + jField.getName() + " is final");
                    }
                }

                try {
                    Object val = popOperandStackVal();
                    jc.getStaticVars()[jField.getSlotId()].setVal(val);
                    initClass(jThread, jc);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            }
            case GETFIELD: {
                JField jField = getField(runtimeConstantPool);
                JObject ref = popOperandStackRefVal();
                pushOperandStackObjVal(ref.getFields()[jField.getSlotId()].getVal());
                break;
            }
            case PUTFIELD: {
                JField jField = getField(runtimeConstantPool);
                /**
                 * 不能是static变量
                 */
                if (jField.isStatic()) {
                    throw new IncompatibleClassChangeError("the field:" + jField.getName() + " is static ");
                }
                /**
                 * final变量只能在<init>方法初始化
                 */
                if (jField.isFinal()) {
                    if (!getJMethod().getName().equals("<init>")) {
                        throw new IllegalAccessError("the method:" + getJMethod().getName() + " is not <init>");
                    }
                }
                Object val = popOperandStackVal();
                JObject ref = popOperandStackRefVal();
                ref.getFields()[jField.getSlotId()].setVal(val);
                break;
            }
            case INVOKEVIRTUAL: {
                JMethod method = getMethod(runtimeConstantPool);
                /**
                 * 定位到this的位置，创建栈帧时this在局部变量表中第0个位置
                 * 根据this查找虚方法
                 */
                JObject ref = (JObject) getOperandStackVal(method.getArgSlotCount());
                if (ref == null) {
                    throw new NullPointerException();
                }
                JMethod vMethod = ref.getJClass().getVtableMap().get(method.getName() + method.getDescriptor());
                if (vMethod == null) {
                    throw new NoSuchMethodError("not found " + method.getName());
                }
                method = vMethod;
                if (method.isStatic()) {
                    throw new IncompatibleClassChangeError("the method:" + method.getName() + " is static");
                }
                if (method.isAbstract()) {
                    throw new AbstractMethodError("the method:" + method.getName() + " is abstract");
                }
                /**
                 * 判断是否本地方法
                 */
                if (method.isNative()) {
                    createNativeFrame(jThread, method);
                } else {
                    jThread.createStackFrame(method);
                }
                break;
            }
            case INVOKESPECIAL: {
                JMethod method = getMethod(runtimeConstantPool);
                if (method.isNative()) {
                    createNativeFrame(jThread, method);
                } else {
                    JClass jc = method.getJClass();
                    if (method.getName().equals("<init>")) {
                        if (!(jc.isSuper() || currentClass.equals(jc))) {
                            throw new NoSuchMethodError("<init> is not in " + jc.getClassName());
                        }
                    }
                    if (method.isStatic()) {
                        throw new IncompatibleClassChangeError("the method:" + method.getName() + " is static");
                    }
                    jThread.createStackFrame(method);
                }
                break;
            }
            case INVOKESTATIC: {
                JMethod method = getMethod(runtimeConstantPool);
                JClass jc = method.getJClass();
                if (method.getName().equals("loadLibrary")) {
                    return;
                }
                if (method.isNative()) {
                    createNativeFrame(jThread, method);
                } else {
                    jThread.createStackFrame(method);
                }
                try {
                    initClass(jThread, jc);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                break;
            }
            case INVOKEINTERFACE: {
                JMethod method = getInterfaceMethod(runtimeConstantPool);
                if (method.isStatic() || method.isPrivate()) {
                    throw new IncompatibleClassChangeError();
                }
                JObject ref = (JObject) getOperandStackVal(method.getArgSlotCount());
                if (ref == null) {
                    throw new NullPointerException();
                }
                if (!JClass.isImplements(ref.getJClass(), method.getJClass())) {
                    throw new IncompatibleClassChangeError();
                }
                method = ref.getJClass().getVtableMap().get(method.getName() + method.getDescriptor());
                if (method == null || method.isAbstract()) {
                    throw new AbstractMethodError();
                }
                jThread.createStackFrame(method);
                break;
            }
            case INVOKEDYNAMIC: {
                break;
            }
            case NEW: {
                try {
                    int classIndex = parseAndReadValU2(false);
                    JClassRef classRef = (JClassRef) currentClass.getConstantPool().getConstantVal(classIndex);
                    classRef = new JClassRef(classRef);
                    JClass jc = classRef.getJClass();
                    if (jc.getState().equals(JClassState.FULLY_INITIALIZED) || jc.getState().equals(JClassState.BEING_INITIALIZED)) {
                        offerIncrement(3);
                        JObject jObject = classRef.getJClass().newJObject();
                        pushOperandStackVal(jObject);
                    } else {
                        initClass(jThread, classRef.getJClass());
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                break;
            }
            case NEWARRAY: {
                Integer baseArrayType = parseAndReadValU1();
                String className = ArrayType.getArrayType(baseArrayType).getClassTypeName();
                try {
                    JClass arrJClass = currentClass.getLoader().loadJClass(className);
                    JObject arrJObject = JObject.newJArray(arrJClass, popOperandStackIntVal());
                    pushOperandStackVal(arrJObject);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
            case ANEWARRAY: {
                int classIndex = parseAndReadValU2();
                JClassRef classRef = (JClassRef) runtimeConstantPool.getConstantVal(classIndex);
                try {
                    classRef = new JClassRef(classRef);
                    JObject arrJObject = classRef.getJClass().newJArray(popOperandStackIntVal());
                    pushOperandStackVal(arrJObject);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                break;
            }
            case ARRAYLENGTH: {
                JObject ref = popOperandStackRefVal();
                if (ref == null) {
                    throw new NullPointerException();
                }
                pushOperandStackVal(ArrayUtils.getLength(ref.getData()));
                break;
            }
            case ATHROW: {//抛出异常信息
                JObject ref = popOperandStackRefVal();
                if (ref == null) {
                    throw new NullPointerException();
                }
                while (!jThread.getJvmStack().isEmpty()) {
                    //回到上一个指令
                    int pc = jThread.getNextPc().get() - 1;
                    int handlerPC = getJMethod().findExceptionHandler(ref.getJClass(), pc);
                    if (handlerPC != -1) {
                        getStackFrame().getOperandStack().getStack().clear();
                        offerIncrement(handlerPC - 1);
                        break;
                    }
                    jThread.getJvmStack().pop();
                }
                if (jThread.getJvmStack().isEmpty()) {
                    JField jField = ref.getJClass().getJField("detailMessage", "Ljava/lang/String;");
                    String msg = new String((char[]) ((JObject) (((JObject) ref.getField(jField.getSlotId()).getVal())).getFields()[0].getVal()).getData());
                    System.err.println(ref.getJClass().getClassName() + ": " + msg);
                    end();
                }
                break;
            }

            case CHECKCAST: {
                JObject ref = popOperandStackRefVal();
                pushOperandStackVal(ref);
                int index = parseAndReadValU2();
                if (ref == null) {
                    return;
                }
                JClassRef jClassRef = (JClassRef) runtimeConstantPool.getConstantVal(index);
                try {
                    jClassRef = new JClassRef(jClassRef);
                    if (!JClass.isAssignableFrom(ref, jClassRef.getJClass())) {
                        throw new ClassCastException();
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                break;
            }
            case INSTANCEOF: {
                try {
                    JObject ref = popOperandStackRefVal();
                    int index = parseAndReadValU2();
                    if (ref == null) {
                        pushOperandStackVal(0);
                    } else {
                        JClassRef jClassRef = (JClassRef) runtimeConstantPool.getConstantVal(index);
                        jClassRef = new JClassRef(jClassRef);
                        if (JClass.isAssignableFrom(ref, jClassRef.getJClass())) {
                            pushOperandStackVal(1);
                        } else {
                            pushOperandStackVal(0);
                        }
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                break;
            }
            case MONITORENTER:
            case MONITOREXIT: {
                popSlot();
                break;
            }
        }
    }

    private JField getField(RuntimeConstantPool runtimeConstantPool, boolean isMovePC) {
        int index = parseAndReadValU2(isMovePC);
        JFieldRef fieldRef = (JFieldRef) runtimeConstantPool.getConstantVal(index);
        try {
            fieldRef = new JFieldRef(fieldRef);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return fieldRef.getJField();
    }

    private JField getField(RuntimeConstantPool runtimeConstantPool) {
        return getField(runtimeConstantPool, true);
    }

    private JMethod getInterfaceMethod(RuntimeConstantPool runtimeConstantPool) {
        JClass currentJClass = getStackFrame().getJClass();
        Integer methodIndex = parseAndReadValU2();
        int count = parseAndReadValU1();
        int zero = parseAndReadValU1();
        JInterfaceMethodRef methodRef = (JInterfaceMethodRef) runtimeConstantPool.getConstantVal(methodIndex);
        try {
            methodRef = new JInterfaceMethodRef(currentJClass, methodRef);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        return methodRef.getJMethod();
    }

    private JMethod getMethod(RuntimeConstantPool runtimeConstantPool) {
        JClass currentJClass = getStackFrame().getJClass();
        Integer methodIndex = parseAndReadValU2();
        JMethodRef methodRef = (JMethodRef) runtimeConstantPool.getConstantVal(methodIndex);
        try {
            methodRef = new JMethodRef(currentJClass, methodRef);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return methodRef.getJMethod();
    }

    /**
     * 执行<clinit>方法
     *
     * @param jThread
     * @param jc
     */
    private void initClass(JThread jThread, JClass jc) throws ClassNotFoundException {
        synchronized (jc) {
            if (!jc.getState().equals(JClassState.FULLY_INITIALIZED) && !jc.getState().equals(JClassState.BEING_INITIALIZED)) {
                Deque<JMethod> jMethods = jc.initClinit();
                while (!jMethods.isEmpty()) {
                    JMethod clinitMethod = jMethods.poll();
                    jThread.createStackFrame(new JMethodRef(clinitMethod.getJClass(), clinitMethod).getJMethod());
                }
            }
        }
    }

    private void createNativeFrame(JThread jThread, JMethod jMethod) {
        jMethod.setMaxLocals(jMethod.getArgSlotCount());
        jMethod.setMaxStack(4);
        StringBuilder code = new StringBuilder("FE");
        ClassUtil.getClassByDescriptor(jMethod.getDescriptor());
        switch (jMethod.getDescriptor().charAt(jMethod.getDescriptor().length() - 1)) {
            case 'V': {// return
                code.append("B1");
                break;
            }
            case 'D': {// dreturn
                code.append("AF");
                break;
            }
            case 'F': {// freturn
                code.append("AE");
                break;
            }
            case 'J': {// lreturn
                code.append("AD");
                break;
            }
            case 'C':
            case 'S':
            case 'B':
            case 'Z':
            case 'I': {// ireturn
                code.append("AC");
                break;
            }
            default: {// areturn
                code.append("B0");
                break;
            }
        }
        jMethod.setCode(new InstructionCode(code.toString()));
        jThread.createStackFrame(jMethod);
    }
}